8  API Authentication

The current NTLM authentication protocol is ‘end of life’ in early 2027, so we need to update the API authentication layer to a more modern protocol. The company uses AD Entra.

8.0.1 Flow 1 — Human SSO (Entra bearer token, domain-joined device)

sequenceDiagram
    participant Browser
    participant IIS
    participant EntraMiddleware as EntraTokenValidationMiddleware
    participant AuthHandler as AppAuthorizationMessageHandler
    participant Resolver as AdIdentityResolver
    participant Domain as PrincipalUserQuery (MediatR)

    Browser->>IIS: GET /api/... (no explicit login)<br/>MSAL silently acquires token via PRT/Seamless SSO
    Note over Browser,IIS: Authorization: Bearer eyJ...
    IIS->>EntraMiddleware: OWIN pipeline
    EntraMiddleware->>EntraMiddleware: Validate JWT signature via OIDC discovery keys
    EntraMiddleware->>EntraMiddleware: Stamp ClaimsPrincipal with kf_auth_source=Bearer
    EntraMiddleware->>EntraMiddleware: Set Thread.CurrentPrincipal + HttpContext.Current.User
    EntraMiddleware->>AuthHandler: next.Invoke()
    AuthHandler->>Resolver: ResolveAccountName(request)
    Resolver->>Resolver: Detect kf_auth_source=Bearer claim
    Resolver->>Resolver: Extract preferred_username → upn → email
    Resolver->>Resolver: MapToAccountName() → internal AD login
    Resolver-->>AuthHandler: "SA1\DOEJO"
    AuthHandler->>Domain: PrincipalUserQuery { ActiveDirectoryAccount="SA1\DOEJO" }
    Domain-->>AuthHandler: PrincipalUser
    AuthHandler-->>Browser: 200 OK

8.0.2 Flow 2 — Human SSO (Windows / NTLM fallback, mode = Dual)

sequenceDiagram
    participant Browser
    participant IIS
    participant EntraMiddleware as EntraTokenValidationMiddleware
    participant AuthHandler as AppAuthorizationMessageHandler
    participant Resolver as AdIdentityResolver
    participant Domain as PrincipalUserQuery (MediatR)

    Browser->>IIS: GET /api/... (no bearer token)
    Note over Browser,IIS: IIS Windows Auth negotiates NTLM/Kerberos
    IIS->>EntraMiddleware: OWIN pipeline
    EntraMiddleware->>EntraMiddleware: No Authorization: Bearer header → no-op
    EntraMiddleware->>AuthHandler: next.Invoke()
    AuthHandler->>Resolver: ResolveAccountName(request)
    Resolver->>Resolver: No kf_auth_source=Bearer claim
    Resolver->>Resolver: Mode = Dual + EnableWindowsFallback = true
    Resolver->>Resolver: Read HttpContext.Current.User.Identity.Name
    Resolver-->>AuthHandler: "SA1\DOEJO"
    AuthHandler->>Domain: PrincipalUserQuery { ActiveDirectoryAccount="SA1\DOEJO" }
    Domain-->>AuthHandler: PrincipalUser
    AuthHandler-->>Browser: 200 OK

8.0.3 Flow 3 — System-to-system (existing ExternalApplicationHandler — unchanged)

sequenceDiagram
    participant CallingService as Calling Service
    participant IIS
    participant EntraMiddleware as EntraTokenValidationMiddleware
    participant AuthHandler as AppAuthorizationMessageHandler
    participant ExtHandler as ExternalApplicationHandler
    participant Domain as PrincipalUserQuery (MediatR)

    CallingService->>IIS: POST /api/... + cert / shared-secret header
    IIS->>EntraMiddleware: OWIN pipeline
    EntraMiddleware->>EntraMiddleware: No bearer token (or token ignored) → no-op
    EntraMiddleware->>AuthHandler: next.Invoke()
    AuthHandler->>ExtHandler: IsRequestFromExternalApplication()?
    ExtHandler-->>AuthHandler: true
    AuthHandler->>ExtHandler: GetAuthorizedUserName()
    ExtHandler-->>AuthHandler: "svc-account@knightfrank.com"
    AuthHandler->>Domain: PrincipalUserQuery { ActiveDirectoryAccount="svc-account@..." }
    Domain-->>AuthHandler: PrincipalUser
    AuthHandler-->>CallingService: 200 OK
    Note over AuthHandler,ExtHandler: Entire resolver path is bypassed.<br/>No change from current behaviour.

8.1 Current state in your API

  1. Windows auth is enabled in config: Web.config:630
  2. Anonymous users are denied: Web.config:631
  3. User identity is read from IIS Windows principal name in the auth handler: AppAuthorizationMessageHandler.cs
  4. SignalR does the same pattern: SignalrAuthorizationAttribute.cs

8.3 How users still avoid login prompts

  1. Domain-joined or Entra-joined devices with Seamless SSO/PRT can get tokens silently.
  2. Browser sessions use SSO cookies/PRT and usually do not show login pages.
  3. Non-compliant devices can still be handled with Conditional Access rules.

8.4 What to change in this solution

Phase 1: Add token auth without breaking NTLM 1. Add OWIN JWT bearer middleware in startup. 1. Keep current handler, but make identity source pluggable: * If bearer token exists, read from claims like preferred_username or upn. * Else fallback to existing HttpContext.Current.User.Identity.Name. 1. Keep existing webhook/system-account paths as-is in the handler.

8.5 Phase 2: Switch primary auth to AD token SSO

  1. Change API config from Windows auth requirement to token validation path.
  2. Update frontend to use MSAL and call API with bearer tokens.
  3. Update SignalR auth to validate token/claims (or use negotiated auth compatible with your SignalR version).

8.6 Phase 3: retire NTLM

  1. Disable IIS Windows authentication for the API.
  2. Enable anonymous requests at IIS layer.
  3. Rely on JWT middleware and API authorization.
  4. Remove NTLM-specific code paths when stable.

8.7 Minimal code design change

  1. Introduce an identity resolver abstraction:
    • ResolveAccountNameFromTokenOrWindowsPrincipal()
  2. Use that in:
    • AppAuthorizationMessageHandler.cs
    • SignalrAuthorizationAttribute.cs
  3. Keep your existing PrincipalUserQuery flow unchanged so permissions/business logic remain stable.

8.8 Claims mapping guidance

  1. Prefer claim order:
    • preferred_username
    • upn
    • email
  2. If your database stores SAM format like DOMAIN, add a mapping step from UPN to SAM where needed.
  3. Store Entra object id as a stable alternate key for future-proofing.

8.9 Risk points to plan for

  1. Legacy browsers and old WebView clients may not perform silent SSO cleanly.
  2. Service-to-service and webhook flows must remain certificate or app-token based.
  3. SignalR auth flow may need special handling depending on transport and token forwarding.
  4. Cross-domain frontend/API requires strict CORS + token audience checks.

8.10 Suggested rollout pattern

  1. Dual-auth window: support NTLM and bearer for a short period.
  2. Pilot one department.
  3. Monitor unauthorized responses and claim-mapping misses.
  4. Complete cutover and disable NTLM.